-
Notifications
You must be signed in to change notification settings - Fork 49.8k
Use HostContext to warn about invalid View/Text nesting #12766
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| parentHostContext: HostContext, | ||
| type: string, | ||
| ): HostContext { | ||
| return {type}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't need this in PROD, let's avoid the extra allocation? Like ReactDOM does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for the root one.
| parentHostContext: HostContext, | ||
| type: string, | ||
| ): HostContext { | ||
| return {type}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is providing a new context object for every node in the tree. We should only do this in DEV and even in DEV we should only do this when we are switching between useful context differences (e.g. from non-text to text). In DOM we change the context only when we switch between HTML and SVG, not every node along the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yah, hadn't yet pushed the DEV only check, but it's there now.
Interesting suggestion with the second bit. I'll add that.
sebmarkbage
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All changes now needs to be replicated in the Fabric renderer too.
Details of bundled changes.Comparing: fc3777b...2ca4555 react-native-renderer
Generated by 🚫 dangerJS |
Ah, right. Okay~ added to fabric too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the flaw here is that currently it's not just a DEV only thing. Its native protocol is contextual. So even if you add this host context, you can't remove the context from the wrapper.
It renders different components depending on where you are. @shergin is fixing this on iOS Fabric to not be needed but currently it's needed on the Android side.
I think the solution to that is to fix the native side so that the difference between RCTVirtualText and RCTText isn't necessary to deal with in JS. Maybe. Although it might be nice to be able to optimize from JS.
| if (__DEV__) { | ||
| const oldIsInAParentText = parentHostContext.isInAParentText; | ||
| const newIsInAParentText = | ||
| type === 'RCTText' || type === 'RCTVirtualText'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new parent can never be a RCTVirtualText. Because of this:
Which highlights that this is in fact not just a DEV only contextual thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new parent can never be a RCTVirtualText.
I'm not sure I understand what you're saying here.
getChildHostContext() can get called with type === "RCTVirtualText" in cases like:
<Text>
<Text>this is inside of a virtual text</Text>
</Text>Which highlights that this is in fact not just a DEV only contextual thing.
I don't understand. The "DEV only" bit here is the warning, not the runtime behavior.
Agreed, but I think the DEV warning about Edit I think you're pointing out that even after these changes, the context API DEV bits in the e.g. |
| type: string, | ||
| ): HostContext { | ||
| if (__DEV__) { | ||
| const oldIsInAParentText = parentHostContext.isInAParentText; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this way vs what you had before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid creating new wrapper objects if isInAParentText hasn't changed.
|
The issue is that if we still need the context in user space, what do we gain by adding it in the reconciler? Other than slightly more overhead. What you could do is make this a prod context too and use it to swap to RCTVirtualText automatically in the renderer instead of in the wrapper. It's a bit unfortunate because we have to add conditionals that gets tested for every kind of view, not just text. |
| type: string, | ||
| ): HostContext { | ||
| if (__DEV__) { | ||
| const oldIsInAParentText = parentHostContext.isInAParentText; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're only going in one direction. If oldIsInAParentText is true, you can bail out early.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't Text > View > Text valid while Text > View > (plain text) isn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Text -> View is not valid.
(It currently is for iOS but it soon won't be. Tim is changing this now.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @yungsters in case this is not correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait why? What do you do when you need to put a view inline in some text?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certain types of components (like Image) are allowed inside of Text - but RCTView itself (the <View> component) is not.
Hm. But I think I see what you're saying. Only treating this value as a one-way thing would suggest that this is allowed: <Text> -> <Image> -> "foo" (although this specific case would have a different error about Image not accepting children...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is a better example of why we shouldn't only set this value one way:
<Text>
<ScrollView>
foo
</ScrollView>
</Text>
I don't think this is valid. We should warn.
I'm going to add a test for this and back out the one-way change.
Fair question! I pushed this kind of for discussion purposes more so than thinking it's definitely a good idea. That being said, if you look at a diff like D7895382, we could remove the We could maybe also move the |
I don't think there is disagreement on mitigating the need to deal with this in JS. In fact, I was just discussing this with @shergin and @mdvacca today. With the new UIManager implementations and our desire to re-think some of the core primitives, I see this being only a matter of time.
This pull request actually adds two checks to the reconciler:
The first check is definitely redundant today and we do not gain anything from adding it. The only thing that we get is that once all the native components no longer require the use of context at runtime, we'll also be able to clean up the check from The second check is actually not possible today in user space (e.g. I hope this makes sense. |
It did 😄 |
…nticipation of Tim removing this change on RN
|
I've removed |
yungsters
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome. Thanks again for helping us make this much easier to debug in React Native.
|
|
||
| type HostContext = { | ||
| isInAParentText: boolean, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In React Native, we've started adopting a common practice to use:
type HostContext = $ReadOnly<{|
isInAParentText: boolean,
|}>;
This makes it so you cannot write to isInAParentText and disallows access of properties other than the ones supplied. But I'd rather you use what is common in the React repository.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I've been trying to use exact types when I think of it. I dig the $ReactOnly suggestion too.
| getRootHostContext(): {} { | ||
| return emptyObject; | ||
| getRootHostContext(rootContainerInstance: Container): HostContext { | ||
| return __DEV__ ? {isInAParentText: false} : emptyObject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised that emptyObject fulfills the type definition for HostContext. I would've expected isInAParentText to need to be optional (isInAParentText?: boolean), but maybe emptyObject is typed with something lossy like Object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true. I'd guess emptyObject is resolving to an any type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually something more insidious is going on here. I can't make Flow report an error in this type no matter what I do, within the React native or fabric renderers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, given this type:
type HostContext = $ReadOnly<{|
isInAParentText: boolean,
|}>;Flow reports "no errors" if I add these lines inside of a method like createInstance() that accepts a hostContext: HostContext param:
hostContext.isInAParentText = false;But if I change it to this, Flow fails:
(hostContext: HostContext).isInAParentText = false;Covariant property
isInAParentTextincompatible with contravariant use in assignment of propertyisInAParentText
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the React repo is using flow-cli v0.61 and the latest version is v0.72 ~ not sure if this could be an old Flow bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolution: I was trying to reproduce inside of a conditional, and we're using an older version of Flow that has a bug (reproducible in the REPL) that does not properly handle this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:o Should we upgrade Flow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I'll add this discussion point to our sync notes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a failing case:
type HostContext = $ReadOnly<{|
isInAParentText: boolean
|}>;
const hostContext: HostContext = {
isInAParentText: true
};
if (hostContext.thisPropertyDoesNotExist) {
// And this one is a Boolean and should not be writable
hostContext.isInAParentText = 'abc';
}|
Based on an in-person conversation yesterday with Tim and Kevin about future RN direction, I've replaced the DEV-only checks that logged to Back to @yungsters and @sebmarkbage for review and consideration. |
yungsters
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. Thanks!
I am afraid, it is used in Fabric, and I don't plan to remove it. (In the first version of this component in Fabric that condition looked a bit different, but now it is identical to Paper.) Even if it is probably possible, it is far from trivial and probably overcomplicate several things on the native side. Ideally, the |
|
@shergin Sending both Text and VirtualText for a single non-nested Text is an easy change to make on the JS side you prefer the two separated on the native side. |
|
Yeah, sure. |
|
Thanks all. Once RN is ready on the native side for us to do more cleanup, let's do it! I'm happy to make more changes on the renderer side to enable this. |
React Native currently uses the context API to warn about invalid nesting of
TextandViewcomponents. Some of this is currently necessary because of conditional logic like this. Other parts could be moved here though (likeViewwarning if it's within aTextancestor) in order to remove the necessity of using an extraContext.Consumercomponent. Planned changes to React Native may also allow for moving otherContext.Consumercomponents from the JS side, which would make this warning approach more attractive.cc @yungsters